/**
 * Copyright (c) 2008-2010 Ardor Labs, Inc.
 *
 * This file is part of Ardor3D.
 *
 * Ardor3D is free software: you can redistribute it and/or modify it 
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or at <http://www.ardor3d.com/LICENSE>.
 */

package name.roslan.games.moo3d.input.control;

import com.ardor3d.framework.Canvas;
import com.ardor3d.input.Key;
import com.ardor3d.input.KeyboardState;
import com.ardor3d.input.MouseState;
import com.ardor3d.input.logical.InputTrigger;
import com.ardor3d.input.logical.LogicalLayer;
import com.ardor3d.input.logical.TriggerAction;
import com.ardor3d.input.logical.TriggerConditions;
import com.ardor3d.input.logical.TwoInputStates;
import com.ardor3d.math.Matrix3;
import com.ardor3d.math.Vector3;
import com.ardor3d.math.type.ReadOnlyVector3;
import com.ardor3d.renderer.Camera;
import com.ardor3d.scenegraph.Node;
import com.ardor3d.scenegraph.extension.CameraNode;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;

/**
 * A modified FirstPersonControl to allow setting the camera behind the protagonist.
 * 
 * @author Roslan Amir
 * @version 1.0 - May 4, 2012
 */
public class ThirdPersonControl {

    private final Vector3 _upAxis = new Vector3();
    private double _mouseRotateSpeed = .005;

    private double _moveSpeed = 50;
    private double _keyRotateSpeed = 2.25;
    private final Matrix3 _workerMatrix = new Matrix3();
    private final Vector3 _workerStoreA = new Vector3();
    private InputTrigger _mouseTrigger;
    private InputTrigger _keyTrigger;

    public Node _cameraRotationNode;
    public CameraNode _cameraTranslationNode = new CameraNode();

    private ThirdPersonControl(final ReadOnlyVector3 upAxis, final Node cameraRotationNode, final Camera cam, final boolean updateFromCamera) {
        _upAxis.set(upAxis);
        _cameraRotationNode = cameraRotationNode;
        // cam.set(new Camera()); // reset all previous settings
        _cameraTranslationNode.setCamera(cam);
        if (updateFromCamera) {
            _cameraTranslationNode.updateFromCamera();
        } else {
            _cameraTranslationNode.setTranslation(new Vector3(0, 0, 20));
            final Matrix3 rotation = new Matrix3(_cameraTranslationNode.getRotation());
            rotation.setValue(0, 0, -1);
            rotation.setValue(1, 1, 1);
            rotation.setValue(2, 2, -1);
            // rotation.lookAt(new Vector3(0, 0, 21), Vector3.NEG_UNIT_Y);
            _cameraTranslationNode.setRotation(rotation);
        }

        // cam.
        // _cameraTranslationNode.updateFromCamera();
        // cameraTranslationNode.setTranslation(new Vector3(0, 40, 20));
        cameraRotationNode.attachChild(_cameraTranslationNode);
        cameraRotationNode.updateWorldTransform(true);
    }

    public ReadOnlyVector3 getUpAxis() {
        return _upAxis;
    }

    public void setUpAxis(final ReadOnlyVector3 upAxis) {
        _upAxis.set(upAxis);
    }

    public double getMouseRotateSpeed() {
        return _mouseRotateSpeed;
    }

    public void setMouseRotateSpeed(final double speed) {
        _mouseRotateSpeed = speed;
    }

    public double getMoveSpeed() {
        return _moveSpeed;
    }

    public void setMoveSpeed(final double speed) {
        _moveSpeed = speed;
    }

    public double getKeyRotateSpeed() {
        return _keyRotateSpeed;
    }

    public void setKeyRotateSpeed(final double speed) {
        _keyRotateSpeed = speed;
    }

    Vector3 temp = new Vector3();

    Vector3 getLeft(final Node n) {
        return n.getRotation().getColumn(0, temp);
    }

    Vector3 getUp(final Node n) {
        return n.getRotation().getColumn(1, temp);
    }

    Vector3 getDir(final Node n) {
        return n.getRotation().getColumn(2, temp);
    }

    Matrix3 tempM = new Matrix3();

    void setLeft(final Node n, final ReadOnlyVector3 v) {
        tempM.setColumn(0, v);
        n.setRotation(tempM);
    }

    void setUp(final Node n, final ReadOnlyVector3 v) {
        tempM.setColumn(1, v);
        n.setRotation(tempM);
    }

    void setDir(final Node n, final ReadOnlyVector3 v) {
        tempM.setColumn(2, v);
        n.setRotation(tempM);
    }

    protected void move(final Camera camera, final KeyboardState kb, final double tpf) {
        // MOVEMENT
        int moveFB = 0, strafeLR = 0;
        if (kb.isDown(Key.W)) {
            moveFB -= 1;
        }
        if (kb.isDown(Key.S)) {
            moveFB += 1;
        }
        if (kb.isDown(Key.A)) {
            strafeLR -= 1;
        }
        if (kb.isDown(Key.D)) {
            strafeLR += 1;
        }

        if (moveFB != 0 || strafeLR != 0) {
            final Vector3 loc = _workerStoreA.zero();
            if (moveFB == 1) {
                loc.addLocal(getDir(_cameraRotationNode));
            } else if (moveFB == -1) {
                loc.subtractLocal(getDir(_cameraRotationNode));
            }
            if (strafeLR == 1) {
                loc.addLocal(getLeft(_cameraRotationNode));
            } else if (strafeLR == -1) {
                loc.subtractLocal(getLeft(_cameraRotationNode));
            }
            loc.normalizeLocal().multiplyLocal(_moveSpeed * tpf).addLocal(_cameraRotationNode.getTranslation());
            _cameraRotationNode.setTranslation(loc);
            _cameraRotationNode.updateWorldTransform(true);
        }

        // ROTATION
        int rotX = 0, rotY = 0;
        if (kb.isDown(Key.UP)) {
            rotY -= 1;
        }
        if (kb.isDown(Key.DOWN)) {
            rotY += 1;
        }
        if (kb.isDown(Key.LEFT)) {
            rotX += 1;
        }
        if (kb.isDown(Key.RIGHT)) {
            rotX -= 1;
        }
        if (rotX != 0 || rotY != 0) {
            rotate(camera, rotX * (_keyRotateSpeed / _mouseRotateSpeed) * tpf, rotY * (_keyRotateSpeed / _mouseRotateSpeed) * tpf);
        }
    }

    protected void rotate(final Camera camera, final double dx, final double dy) {

        if (dx != 0) {
            _workerMatrix.fromAngleNormalAxis(_mouseRotateSpeed * dx, _upAxis != null ? _upAxis : getUp(_cameraRotationNode));
            _workerMatrix.applyPost(getLeft(_cameraRotationNode), _workerStoreA);
            setLeft(_cameraRotationNode, _workerStoreA);
            _workerMatrix.applyPost(getDir(_cameraRotationNode), _workerStoreA);
            setDir(_cameraRotationNode, _workerStoreA);
            _workerMatrix.applyPost(getUp(_cameraRotationNode), _workerStoreA);
            setUp(_cameraRotationNode, _workerStoreA);
        }

        if (dy != 0) {
            _workerMatrix.fromAngleNormalAxis(_mouseRotateSpeed * dy, getLeft(_cameraRotationNode));
            _workerMatrix.applyPost(getLeft(_cameraRotationNode), _workerStoreA);
            setLeft(_cameraRotationNode, _workerStoreA);
            _workerMatrix.applyPost(getDir(_cameraRotationNode), _workerStoreA);
            setDir(_cameraRotationNode, _workerStoreA);
            _workerMatrix.applyPost(getUp(_cameraRotationNode), _workerStoreA);
            setUp(_cameraRotationNode, _workerStoreA);
        }

        /*
         * final Quaternion transformAngle = Quaternion.fetchTempInstance();
         * 
         * transformAngle .fromEulerAngles((xAngle -= x) * MathUtils.DEG_TO_RAD, 0.0D, (yAngle += y) * MathUtils.DEG_TO_RAD);
         * _cameraRotationNode.setRotation(transformAngle); // transformAngle.apply(translation, translation); // main.cameraNode.setTranslation(translation);
         * Quaternion.releaseTempInstance(transformAngle);
         */
        _cameraRotationNode.updateWorldTransform(true);
    }

    /**
     * @param layer the logical layer to register with
     * @param upAxis the up axis of the camera
     * @param dragOnly if true, mouse input will only rotate the camera if one of the mouse buttons (left, center or right) is down.
     * @return a new ThirdPersonControl object
     */
    public static ThirdPersonControl setupTriggers(final LogicalLayer layer, final ReadOnlyVector3 upAxis, final boolean dragOnly, final Node controlNode, final Camera cam, final boolean updateFromCamera) {

        final ThirdPersonControl control = new ThirdPersonControl(upAxis, controlNode, cam, updateFromCamera);
        control.setupKeyboardTriggers(layer);
        control.setupMouseTriggers(layer, dragOnly);
        return control;
    }

    /**
     * Deregister the triggers of the given ThirdPersonControl from the given LogicalLayer.
     * 
     * @param layer
     * @param control
     */
    public static void removeTriggers(final LogicalLayer layer, final ThirdPersonControl control) {
        if (control._mouseTrigger != null) {
            layer.deregisterTrigger(control._mouseTrigger);
        }
        if (control._keyTrigger != null) {
            layer.deregisterTrigger(control._keyTrigger);
        }
    }

    public void setupMouseTriggers(final LogicalLayer layer, final boolean dragOnly) {
        final ThirdPersonControl control = this;
        // Mouse look
        final Predicate<TwoInputStates> someMouseDown = Predicates.or(TriggerConditions.leftButtonDown(), Predicates.or(TriggerConditions.rightButtonDown(), TriggerConditions.middleButtonDown()));
        final Predicate<TwoInputStates> dragged = Predicates.and(TriggerConditions.mouseMoved(), someMouseDown);
        final TriggerAction dragAction = new TriggerAction() {

            // Test boolean to allow us to ignore first mouse event. First event can wildly vary based on platform.
            private boolean firstPing = true;

            public void perform(final Canvas source, final TwoInputStates inputStates, final double tpf) {
                final MouseState mouse = inputStates.getCurrent().getMouseState();
                if (mouse.getDx() != 0 || mouse.getDy() != 0) {
                    if (!firstPing) {
                        control.rotate(source.getCanvasRenderer().getCamera(), -mouse.getDx(), -mouse.getDy());
                    } else {
                        firstPing = false;
                    }
                }
            }
        };

        _mouseTrigger = new InputTrigger(dragOnly ? dragged : TriggerConditions.mouseMoved(), dragAction);
        layer.registerTrigger(_mouseTrigger);
    }

    public Predicate<TwoInputStates> setupKeyboardTriggers(final LogicalLayer layer) {

        final ThirdPersonControl control = this;

        // WASD control
        final Predicate<TwoInputStates> keysHeld = new Predicate<TwoInputStates>() {
            Key[] keys = new Key[] { Key.W, Key.A, Key.S, Key.D, Key.LEFT, Key.RIGHT, Key.UP, Key.DOWN };

            public boolean apply(final TwoInputStates states) {
                for (final Key k : keys) {
                    if (states.getCurrent() != null && states.getCurrent().getKeyboardState().isDown(k)) {
                        return true;
                    }
                }
                return false;
            }
        };

        final TriggerAction moveAction = new TriggerAction() {
            public void perform(final Canvas source, final TwoInputStates inputStates, final double tpf) {
                control.move(source.getCanvasRenderer().getCamera(), inputStates.getCurrent().getKeyboardState(), tpf);
            }
        };
        _keyTrigger = new InputTrigger(keysHeld, moveAction);
        layer.registerTrigger(_keyTrigger);
        return keysHeld;
    }

    public InputTrigger getKeyTrigger() {
        return _keyTrigger;
    }

    public InputTrigger getMouseTrigger() {
        return _mouseTrigger;
    }
}
